/*
 * IzPack - Copyright 2001-2008 Julien Ponge, All Rights Reserved.
 *
 * http://izpack.org/
 * http://izpack.codehaus.org/
 *
 * Copyright 2002 Elmar Grom
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.izforge.izpack.util;

import java.io.BufferedReader;
import java.io.File;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.StringTokenizer;
import java.util.logging.Level;
import java.util.logging.Logger;

/*---------------------------------------------------------------------------*/

/**
 * The <code>TargetFactory</code> serves as a central mechanism to instantiate OS specific class
 * flavors, provide OS specific file extension types, default install directories and similar
 * functionality. In addition it provides services that are related to OS versions and flavors. For
 * a tutorial on using some of the features in this class see the <A
 * HREF=doc-files/TargetFactory.html>TargetFactory Tutorial</A>.
 *
 * @author Elmar Grom
 * @version 0.0.1 / 1/3/2002
 */
/*---------------------------------------------------------------------------*/
/*
 * $ @design
 *
 * Reports actually observed on some systems:
 *
 * OS OS Name Version Architecture Native Report (ver)
 * ----------------------------------------------------------------------------------------------------------
 * Windows 95 Windows 98 Windows 98 4.10 x86 Windows 98 [Version 4.10.1998] Windows-ME Windows Me
 * 4.90 x86 Windows Millennium [Version 4.90.3000] Windows-NT 3.5 Windows-NT 4.0 Windows NT 4.0 x86
 * Windows NT Version 4.0 Windows 2000 Windows 2000 5.0 x86 Microsoft Windows 2000 [Version
 * 5.00.2195] Windows-XP Windows 2000 5.1 x86 Microsoft Windows XP [Version 5.1.2600] Windows-XP
 * Windows XP 5.1 x86 Mac Mac OS-X Linux Linux 2.4.7-10 i386 Linux Linux 2.4.18-4GB i386 Solaris
 *
 * ---------------------------------------------------------------------------
 */
public class TargetFactory
{
    private static final Logger logger = Logger.getLogger(TargetFactory.class.getName());

    // ------------------------------------------------------------------------
    // Constant Definitions
    // ------------------------------------------------------------------------

    // Basic operating systems

    /**
     * Identifies Microsoft Windows.
     */
    public static final int WINDOWS = 0;

    /**
     * Identifies generic UNIX operating systems
     */
    public static final int UNIX = 2;

    /**
     * Used to report a non specific operating system.
     */
    public static final int GENERIC = 3;

    // operating system favors

    /**
     * This is the basic flavor for every operating system.
     */
    public static final int STANDARD = 0;

    /**
     * Used to identify the Windows-NT class of operating systems in terms of an OS flavor. It is
     * reported for Windows-NT, 2000 and XP.
     */
    public static final int NT = 1;

    /**
     * Used to identify the OS X flavor of the Mac OS
     */
    public static final int X = 2;

    // system architecture

    /**
     * Identifies Intel X86 based processor types.
     */
    public static final int X86 = 0;

    /**
     * Nonspecific processor architecture, other than X86.
     */
    public static final int OTHER = 1;

    /**
     * The extensions used for native libraries on various operating systems. The string positions
     * correspond to the basic operating system indexes. The following values are legal to use :
     * <br>
     * <br>
     * <ul>
     * <li>WINDOWS
     * <li>MAC
     * <li>UNIX
     * <li>GENERIC
     * </ul>
     */
    static final String[] LIBRARY_EXTENSION = {"dll", "so", "", ""};

    /**
     * The os specific class prefixes for classes that implement different versions for the various
     * operating systems. The string positions correspond to the basic operating system indexes. The
     * following values are legal to use : <br>
     * <br>
     * <ul>
     * <li>WINDOWS
     * <li>MAC
     * <li>UNIX
     * <li>GENERIC
     * </ul>
     */
    static final String[] CLASS_PREFIX = {"Win_", "Mac_", "Unix_", ""};

    /**
     * The os favor specific class prefixes for classes the implement different versions for various
     * os favors. The string positions correspond to the flavor indexes. The following values are
     * legal to use : <br>
     * <br>
     * <ul>
     * <li>STANDARD
     * <li>NT
     * <li>X
     * </ul>
     */
    static final String[] CLASS_FLAVOR_PREFIX = {"", "NT_", "X_"};

    /**
     * The list of processor architecture specific prefixes. The string positions correspond to the
     * architecture indexes. The following values are leegal to use : <br>
     * <br>
     * <ul>
     * <li>X86
     * <li>OTHER
     * </ul>
     */
    static final String[] CLASS_ARCHITECTURE_PREFIX = {"X86_", // Intel X86
            // architecture
            "U_" // unknown
    };

    /**
     * The list of default install path fragments. Depending on the operating system, a path
     * fragment might represent either a part of the default install path or the entire path to use.
     * For MS-Windows it is always only a part of the full install path. The string positions
     * correspond to the basic operating system indexes. The following values are leegal to use :
     * <br>
     * <br>
     * <ul>
     * <li>WINDOWS
     * <li>MAC
     * <li>UNIX
     * <li>GENERIC
     * </ul>
     */
    static final String[] INSTALL_PATH_FRAGMENT = {"Program Files" + File.separator,
            "/Applications" + File.separator, "/usr/local" + File.separator,
            File.separator + "apps" + File.separator};

    /**
     * This is a list of keys to use when looking for resources that define the default install path
     * to use. The list is organized as two dimensional array of <code>String</code>s. To access
     * the array, denote the first dimension with the operating system index and the second
     * dimension with the flavor index. For example to access the key for Windows-NT use
     * <code>INSTALL_PATH_RESOURCE_KEY[WINDOWS][NT]</code> The array uses a sparse population,
     * that is, not all array locations actually contain a key. Only locations for which a real
     * operating system/flavor combination exists are populated. For example, there is no such thing
     * as <code>INSTALL_PATH_RESOURCE_KEY[UNIX][X]</code>
     */
    static final String[][] INSTALL_PATH_RESOURCE_KEY = {
            // Standard NT X
            {"TargetPanel.dir.windows", "TargetPanel.dir.windows", ""}, // Windows
            {"TargetPanel.dir.mac", "", "TargetPanel.dir.macosx"}, // Mac
            {"TargetPanel.dir.unix", "", ""}, // UNIX
            {"TargetPanel.dir", "", ""} // Generic
    };

    /**
     * The delimiter characters used to tokenize version numbers
     */
    private static final String VERSION_DELIMITER = ".-";

    // ------------------------------------------------------------------------
    // Variable Declarations
    // ------------------------------------------------------------------------

    /**
     * The factory for creating instances.
     */
    private final TargetPlatformFactory factory;

    /**
     * identifies the operating system we are running on
     */
    private int os = -1;

    /**
     * identifies the operating system favor
     */
    private int osFlavor = -1;

    /**
     * identifies the hardware architecture we are running on
     */
    private int architecture = -1;

    /**
     * represents the version number of the target system
     */
    private String version = "";

    /**
     * Constructs a <tt>TargetFactory</tt>.
     *
     * @param factory the factory to delegate to
     */
    public TargetFactory(TargetPlatformFactory factory)
    {
        this.factory = factory;
        version = System.getProperty("os.version");

        // ----------------------------------------------------
        // test for Windows
        // ----------------------------------------------------
        if (OsVersion.IS_WINDOWS)
        {
            os = WINDOWS;
            osFlavor = STANDARD;
            architecture = X86;
            String osName = OsVersion.OS_NAME.toLowerCase();

            if (osName.contains("nt"))
            {
                osFlavor = NT;
            }
            else if (osName.contains("2000"))
            {
                osFlavor = NT;
            }
        }
        // ----------------------------------------------------
        // test for Mac OS
        // ----------------------------------------------------
        else if (OsVersion.IS_OSX)
        {
            os = X;
            osFlavor = STANDARD;
            architecture = OTHER;
        }
        // ----------------------------------------------------
        // what's left should be unix
        // ----------------------------------------------------
        else
        {
            os = UNIX;
            osFlavor = STANDARD;
            architecture = OTHER;
            String osName = OsVersion.OS_NAME.toLowerCase();

            if (osName.contains("x86"))
            {
                architecture = X86;
            }
        }
    }

    /**
     * This method returns an OS specific instance of the requested class.
     * <p/>
     * This implementation delegates to {@link DefaultTargetPlatformFactory}, falling back to {@link #makeObject(String)}
     * if the call fails.
     *
     * @param clazz the
     * @return an OS specific instance of <tt>clazz</tt>
     * @throws Exception for any error
     */
    @SuppressWarnings("unchecked")
    public <T> T makeObject(Class<T> clazz) throws Exception
    {
        T result;
        try
        {
            result = factory.create(clazz);
        } catch (Exception e)
        {
            logger.log(Level.WARNING, e.getMessage(), e);
            result = (T) makeObject(clazz.getName());
        }
        return result;
    }
    /*--------------------------------------------------------------------------*/

    /**
     * This method returns an OS and OS flavor specific instance of the requested class. <br>
     * <br>
     * <b>Class Naming Rules</b><br>
     * Class versions must be named with the OS and OS flavor as prefix. The prefixes are simply
     * concatenated, with the OS prefix first and the flavor prefix second. Use the following OS
     * specific prefixes:<br>
     * <br>
     * <TABLE BORDER=1>
     * <TR>
     * <TH>Operating System</TH>
     * <TH>Prefix</TH>
     * </TR>
     * <TR>
     * <TD>Microsoft Windows</TD>
     * <TD>Win_</TD>
     * </TR>
     * <TR>
     * <TD>Mac OS</TD>
     * <TD>Mac_</TD>
     * </TR>
     * <TR>
     * <TD>UNIX</TD>
     * <TD>UNIX_</TD>
     * </TR>
     * </TABLE><br>
     * For the different OS flavors, use these prefixes:<br>
     * <br>
     * <TABLE BORDER=1>
     * <TR>
     * <TH>OS Flavor</TH>
     * <TH>Prefix</TH>
     * </TR>
     * <TR>
     * <TD>NT</TD>
     * <TD>NT_</TD>
     * </TR>
     * <TR>
     * <TD>Mac OS X</TD>
     * <TD>X_</TD>
     * </TR>
     * </TABLE> <br>
     * <br>
     * <b>Naming Example:</b> <br>
     * <br>
     * For the class <code>MyClass</code>, the specific version for Windows NT must be in the
     * same package as <code>MyClass</code> and the name must be <code>Win_NT_MyClass</code>. A
     * version that should be instantiated for any non-NT flavor would be called
     * <code>Win_MyClass</code>. This would also be the version instantiated on Windows NT if the
     * version <code>Win_NT_MyClass</code> does not exist. <br>
     * <br>
     * <b>The Loading Process</b> <br>
     * <br>
     * The process is completed after the first successful attempt to load a class. <br>
     * <ol>
     * <li>load a version that is OS and OS-Flavor specific
     * <li>load a version that is OS specific
     * <li>load the base version (without OS or OS-Flavor prefix)
     * </ol>
     * <br>
     * See the <A HREF=doc-files/TargetFactory.html>TargetFactory Tutorial</A> for more
     * information.<br>
     * <br>
     *
     * @param name the fully qualified name of the class to load without the extension.
     * @return An instance of the requested class. Note that specific initialization that can not be
     *         accomplished in the default constructor still needs to be performed before the object can be
     *         used.
     * @throws Exception if all attempts to instantiate class fail
     */
    /*--------------------------------------------------------------------------*/
    public Object makeObject(String name) throws Exception
    {
        int nameStart = name.lastIndexOf('.') + 1;
        String packageName = name.substring(0, nameStart);
        String className = name.substring(nameStart, name.length());
        String actualName;

        try
        {
            actualName = packageName + CLASS_PREFIX[os] + CLASS_FLAVOR_PREFIX[osFlavor] + className;
            Class<?> temp = Class.forName(actualName);
            return temp.newInstance();
        }
        catch (Throwable exception1)
        {
            try
            {
                Class<?> temp = Class.forName(packageName + CLASS_PREFIX[os] + className);
                return temp.newInstance();
            }
            catch (Throwable exception2)
            {
                try
                {
                    actualName = name;
                    Class<?> temp = Class.forName(actualName);
                    return temp.newInstance();
                }
                catch (Throwable exception3)
                {
                    throw new Exception("can not instantiate class " + name);
                }
            }
        }
    }

    /*--------------------------------------------------------------------------*/

    /**
     * Returns true if the version in the parameter string is higher than the version of the target
     * os.
     *
     * @param version the version number to compare to
     * @return <code>false</code> if the version of the target system is higher, otherwise
     *         <code>true</code>
     */
    /*--------------------------------------------------------------------------*/
    /*
     * $ @design
     *
     * Version numbers are assumed to be constructed as follows: - a list of one or more numbers,
     * separated by periods as in X.X.X. ... or periods and dashes as in X.X.X-Y. ... - the numbers
     * follow the decimal number system - the left most number is of highest significance
     *
     * The process compares each set of numbers, beginning at the most significant and working down
     * the ranks (this is working left to right). The process is stopped as soon as the pair of
     * numbers compaired is not equal. If the numer for the target system is higher, flase is
     * returned, otherwise true.
     * --------------------------------------------------------------------------
     */
    public boolean versionIsHigher(String version) throws Exception
    {
        StringTokenizer targetVersion = new StringTokenizer(this.version, VERSION_DELIMITER);
        StringTokenizer compareVersion = new StringTokenizer(version, VERSION_DELIMITER);

        int target;
        int compare;

        while (targetVersion.hasMoreTokens() && compareVersion.hasMoreTokens())
        {
            try
            {
                target = Integer.parseInt(targetVersion.nextToken());
                compare = Integer.parseInt(compareVersion.nextToken());
            }
            catch (Throwable exception)
            {
                throw new Exception("error in version string");
            }

            if (compare > target)
            {
                return true;
            }
            else if (target > compare)
            {
                return false;
            }
        }

        return false;
    }

    /*--------------------------------------------------------------------------*/

    /**
     * Returns the index number for the target operating system that was detected.
     *
     * @return an index number for the OS
     * @see #WINDOWS
     * @see #UNIX
     * @see #GENERIC
     */
    /*--------------------------------------------------------------------------*/
    public int getOS()
    {
        return os;
    }

    /*--------------------------------------------------------------------------*/

    /**
     * Returns the index number for the operating system flavor that was detected on the target
     * system.
     *
     * @return an index for the OS flavor
     * @see #STANDARD
     * @see #NT
     * @see #X
     */
    /*--------------------------------------------------------------------------*/
    public int getOSFlavor()
    {
        return osFlavor;
    }

    /*--------------------------------------------------------------------------*/

    /**
     * Returns an index number that identified the processor architecture of the target system.
     *
     * @return an index for the processor architecture
     * @see #X86
     * @see #OTHER
     */
    /*--------------------------------------------------------------------------*/
    public int getArchitecture()
    {
        return architecture;
    }

    /*--------------------------------------------------------------------------*/

    /**
     * Returns the file extension customarily used on the target OS for dynamically loadable
     * libraries.
     *
     * @return a <code>String</code> containing the customary library extension for the target OS.
     *         Note that the string might be empty if there no such specific extension for the target OS.
     */
    /*--------------------------------------------------------------------------*/
    public String getNativeLibraryExtension()
    {
        return LIBRARY_EXTENSION[os];
    }

    /*--------------------------------------------------------------------------*/

    /**
     * Returns the system dependent default install path. This is typically used to suggest an
     * istall path to the end user, when performing an installation. The default install path is
     * assembled form the OS specific path fragment specified in <code>INSTALL_PATH_FRAGMENT</code>,
     * possibly a drive letter and the application name. The user the option to define resources
     * that define default paths which differ from the path fragments defined here. The following
     * resource names will be recognized by this method: <br>
     * <br>
     * <ul>
     * <li><code>TargetPanel.dir.windows</code>
     * <li><code>TargetPanel.dir.macosx</code>
     * <li><code>TargetPanel.dir.unix</code>
     * <li><code>TargetPanel.dir</code> plus the all lower case version of
     * <code>System.getProperty ("os.name")</code>, with all spaces replaced by an underscore
     * ('_').
     * <li><code>TargetPanel.dir</code>
     * </ul>
     *
     * @param appName the name of the application to install. If no specific resource has been set,
     *                then this name will be appended to the OS specific default path fragment.
     * @return the default install path for the target system
     */
    /*--------------------------------------------------------------------------*/
    /*
     * $ @design
     *
     * First try to read a path string from a resource file. This approach allows the user to
     * customize the default install path that is suggested to the end user by IzPack. There are a
     * number of choices for the naming of this resource, so we need to go through a few steps in
     * order to exhaust the different possibilities. If this was not successful we use the default
     * install path that is defined for the operating system we are running on. This path should be
     * expanded by the application name to form the full path that to returne.
     * --------------------------------------------------------------------------
     */
    public String getDefaultInstallPath(String appName)
    {
        String path = null;
        InputStream input;
        String keyFragment = "/res/" + INSTALL_PATH_RESOURCE_KEY[GENERIC][STANDARD];

        // ----------------------------------------------------
        // attempt to get an input stream through a resource
        // based on a key which is specific to the target OS
        // ----------------------------------------------------
        input = getClass().getResourceAsStream("/res/" + INSTALL_PATH_RESOURCE_KEY[os][osFlavor]);

        // ----------------------------------------------------
        // attempt to get an input stream through a resource
        // based on a key which is made specific to the target
        // OS by using the string returned by
        // System.getProperty ("os.name").toLowerCase ()
        // ----------------------------------------------------
        if (input == null)
        {
            String key = OsVersion.OS_NAME.toLowerCase().replace(' ', '_'); // avoid
            // spaces
            // in
            // file
            // names
            key = keyFragment + key.toLowerCase(); // for consistency among
            // TargetPanel res files
            input = TargetFactory.class.getResourceAsStream(key);
        }

        // ----------------------------------------------------
        // attempt to get an input stream through a resource
        // based on a key which is not specific to any target OS
        // ----------------------------------------------------
        if (input == null)
        {
            input = TargetFactory.class.getResourceAsStream(keyFragment);
        }

        // ----------------------------------------------------
        // If we got an input stream try to read the path
        // from the file
        // ----------------------------------------------------
        if (input != null)
        {
            InputStreamReader streamReader;
            BufferedReader reader = null;
            String line;

            try
            {
                streamReader = new InputStreamReader(input);
                reader = new BufferedReader(streamReader);
                line = reader.readLine();

                while (line != null)
                {
                    line = line.trim();
                    if (!"".equals(line))
                    {
                        break;
                    }
                    line = reader.readLine();
                }
                path = line;
            }
            catch (Throwable exception)
            {
            }
            finally
            {
                try
                {
                    if (reader != null)
                    {
                        reader.close();
                    }
                }
                catch (Throwable exception)
                {
                }
            }
        }

        // ----------------------------------------------------
        // if we were unable to obtain a path from a resource,
        // use the default for the traget operating system.
        // ----------------------------------------------------
        if (path == null || "".equals(path))
        {
            path = "";

            // --------------------------------------------------
            // if we run on windows, we need a valid drive letter
            // to put in front of the path. The drive that
            // contains the user's home directory is usually the
            // drive that also contains the install directory,
            // so this seems the best choice here.
            // --------------------------------------------------
            if (os == WINDOWS)
            {
                String home = System.getProperty("user.home");
                // take everything up to and including the first '\'
                path = home.substring(0, home.indexOf(File.separatorChar) + 1);
            }

            path = path + INSTALL_PATH_FRAGMENT[os] + appName;
        }

        return path;
    }

    /**
     * Gets a prefix alias for the current platform. "Win_" on domestic Windows Systems 
     * (Windows XP and 7, etc.), "Win_NT_" on WinNT4 and 2000, Mac on Mac Mac_X on macosx 
     * and Unix_
     *
     * @return a prefix alias for the current platform
     */

    public static String getCurrentOSPrefix()
    {
        String OSName = System.getProperty("os.name").toLowerCase();
        String OSArch = System.getProperty("os.arch").toLowerCase();
        int OS = 0;
        int OSFlavor = 0;
        int OSarchitecture = 0;
        // ----------------------------------------------------
        // test for Windows
        // ----------------------------------------------------
        if (OSName.contains("windows"))
        {
            OS = WINDOWS;
            OSFlavor = STANDARD;
            OSarchitecture = X86;

            if (OSName.contains("nt"))
            {
                OSFlavor = NT;
            }
            else if (OSName.contains("2000"))
            {
                OSFlavor = NT;
            }
        }
        // ----------------------------------------------------
        // test for Mac OS
        // ----------------------------------------------------
        else if (OSName.contains("mac"))
        {
            OS = GENERIC;
            OSFlavor = STANDARD;
            OSarchitecture = OTHER;

            if (OSName.contains("macosx"))
            {
                OSFlavor = X;
            }
        }
        // ----------------------------------------------------
        // what's left should be unix
        // ----------------------------------------------------
        else
        {
            OS = UNIX;
            OSFlavor = STANDARD;
            OSarchitecture = OTHER;

            if (OSArch.contains("86"))
            {
                OSarchitecture = X86;
            }
        }

        return (CLASS_PREFIX[OS] + CLASS_FLAVOR_PREFIX[OSFlavor]);
    }

}
/*---------------------------------------------------------------------------*/
